
#include "config.h"

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <fcntl.h>
#include <unistd.h>

#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>

#include <event.h>

#include <fastcgi.h>

#include "dualfifo.h"
#include "fcgiparse.h"
#include "portable.h"
#include "tunable.h"

#include "connection.h"

enum conn_state
{
	CONN_STATE_IDLE = 0,       // Waiting for valid FCGI packet
	CONN_STATE_PREBUFFER,      // Waiting to get enough data to decide what to do with connection
	CONN_STATE_AUTHENTICATE,   // Waiting on authenticate result
	CONN_STATE_REMOTECONN,     // Waiting for connection to proxy endpoint to succeed
	CONN_STATE_PROXY,          // Proxying data
	CONN_STATE_CLOSE,
};

struct conn_data
{
	unsigned int id;
	
	int  proxySocket;
	int clientSocket;

	enum conn_state state;  // State of connection

	struct event evProxyRead;   // Event structure for libevent
	struct event evProxyWrite;  // Event structure for libevent
	struct event evClientRead;  // Event structure for libevent
	struct event evClientWrite; // Event structure for libevent
		
	dualfifo_t proxyBuf;     // Dynamically allocated buffer for storing proxied data
	dualfifo_t clientBuf;    
	
	char * serverName;	    // Name of server (after STATE_PREBUFFER)
	char * cgiHost;         // Information about remote socket (after STATE_AUTHENTICATE)
};

void connection_teardown( struct conn_data *conn );

char * connection_get_servName( FCGI_Header *request, unsigned int contentLength );
int connection_fcgiConnect( struct conn_data *conn );

void connection_proxyRead ( int proxySocket, short status, void * conn_v );
void connection_proxyWrite( int proxySocket, short status, void * conn_v );

int connection_accept( int newSocket, unsigned int id )
{
	struct conn_data *conn;

	conn = malloc( sizeof( struct conn_data ) );
	if( conn == NULL )
	{
		printf( "Cannot allocate memory for connection\n" );
		
		close( newSocket );
			
		return -1;
	}

	conn->id = id;

	fcntl( newSocket, F_SETFL, O_NONBLOCK );
		
	conn->proxySocket = newSocket;

	event_set( &conn->evProxyRead , conn->proxySocket, EV_READ,  connection_proxyRead, conn );
	event_set( &conn->evProxyWrite, conn->proxySocket, EV_WRITE, connection_proxyWrite, conn );

	// Going to have some issues if we need to buffer more than a meg of data per connection
	dualfifo_new( &conn->proxyBuf,  16*1024, 1024*1024 ); 
	dualfifo_new( &conn->clientBuf, 16*1024, 1024*1024 );
	
	conn->state  = CONN_STATE_IDLE;
	
	conn->serverName = NULL;
	conn->cgiHost = NULL;
	
	event_add( &conn->evProxyRead, NULL );
	
	return 0;
}

void connection_teardown( struct conn_data *conn )
{
	if (conn->clientSocket > 0 || conn->proxySocket > 0)
	{
		return;
	}
	

	#ifdef DEBUG
		printf( "(%i) Connection teardown\n", conn->id );
	#endif
	
	
	dualfifo_free( &conn->proxyBuf );
	dualfifo_free( &conn->clientBuf );
	
	if( conn->serverName )
	{
		free( conn->serverName );
		conn->serverName = NULL;
	}
	
	if( conn->cgiHost )
	{
		free( conn->cgiHost );
		conn->cgiHost = NULL;
	}
	
	free( conn );
}


void connection_proxyRead( int proxySocket, short status, void * conn_v )
{
	struct conn_data * conn = (struct conn_data *) conn_v;
	char   readBuffer[ PROXY_READ_SIZE ];
	size_t bytesRead;
	enum conn_state nextState;
	unsigned int contentLength;
	FCGI_Header *fcgi_request;
	
	nextState = conn->state;
	
	// Phase 1: buffer data out of kernel into local FIFO
	switch( conn->state )
	{
		case CONN_STATE_IDLE:
		case CONN_STATE_PREBUFFER:
		case CONN_STATE_PROXY:
			// Read some data into the buffer
			if( dualfifo_bytesAvailable( &conn->proxyBuf, DUALFIFO_MODE_WRITE ) > PROXY_READ_SIZE )
			{
				bytesRead = read( proxySocket, readBuffer, PROXY_READ_SIZE );
			
				if( bytesRead > 0 )
				{
					dualfifo_write( &conn->proxyBuf, readBuffer, bytesRead );
				}
				else
				{
					conn->state = CONN_STATE_CLOSE;
				}
			}
			break;
		default:
			break;
	}
	
	
	// Phase 2: get header, interpret what it means
	while( (fcgi_request = fcgi_parse( &conn->proxyBuf, &contentLength )) != NULL )
	{
		switch( conn->state )
		{
			case CONN_STATE_IDLE:
				if( fcgi_request->type == FCGI_BEGIN_REQUEST )
				{
					// Out of the twilight zone! Off we go!
					nextState = CONN_STATE_PREBUFFER;
				}
				break;
			case CONN_STATE_PREBUFFER:
				if( fcgi_request->type == FCGI_PARAMS )
				{			
					char *serverName;

					serverName = connection_get_servName( fcgi_request, contentLength );
					
					if( serverName )
					{
						printf( "(%i) Server is %s\n", conn->id, serverName );
						
						conn->serverName = serverName;
						
						nextState = CONN_STATE_AUTHENTICATE;
					}
					
				}
				break;
			default:
				break;
		}
		
		free( fcgi_request );
	
		if( conn->state != nextState )
		{
			printf( "(%i) Debug: connection changing state. %i->%i\n", conn->id, conn->state, nextState );
		}
	
		conn->state = nextState;
	}
	
	// Phase 3: state transitions
	switch( conn->state )
	{
		case CONN_STATE_AUTHENTICATE:
			// Everyone gets a cookie! (we're skipping straight to REMOTECONN)
			conn->cgiHost = strdup( "/tmp/php5-fcgi.sock" );
			
			// Make connection to client
			if( connection_fcgiConnect( conn ) < 0 )
			{
				printf( "(%i) Unable to bring up connection to remote CGI server. Abort\n", conn->id );
				conn->state = CONN_STATE_CLOSE;
			}
			else
			{
				printf( "(%i) Connection succeeded\n", conn->id );
				conn->state = CONN_STATE_REMOTECONN;
			}
			break;
		case CONN_STATE_PROXY:
				event_add( &conn->evClientWrite, NULL );
				break;
		default:
			break;
	}
			
	
	if( conn->state == CONN_STATE_CLOSE )
	{
		printf( "(%i) Going to close socket on app server side\n", conn->id );
		
		event_del( &conn->evProxyRead );
			
		close( conn->proxySocket );
		conn->proxySocket = -1;
	
		connection_teardown( conn );
	}
	else
	{
		event_add( &conn->evProxyRead, NULL );
	}
} 

void connection_proxyWrite( int proxySocket, short status, void * conn_v )
{
	struct conn_data * conn = (struct conn_data *) conn_v;
	char   writeBuffer[ PROXY_WRITE_SIZE ];
	unsigned int bytesToWrite;
	size_t bytesWritten;
	
	bytesToWrite = dualfifo_peek( &conn->clientBuf, DUALFIFO_MODE_BOTH, writeBuffer, PROXY_WRITE_SIZE );	
	if( bytesToWrite == 0 ) return;
		
	bytesWritten = send( proxySocket, writeBuffer, bytesToWrite, 0 );
	if( bytesWritten > 0 )
	{
		dualfifo_advance( &conn->clientBuf, DUALFIFO_MODE_BOTH, bytesWritten );
	}
	
	if( dualfifo_bytesAvailable( &conn->clientBuf, DUALFIFO_MODE_BOTH ) > 0 )
	{
		event_add( &conn->evProxyWrite, NULL );
	}
}
	

void connection_clientRead( int clientSocket, short status, void * conn_v )
{
	struct conn_data * conn = (struct conn_data *) conn_v;
	char   readBuffer[ PROXY_READ_SIZE ];
	size_t bytesRead;
	unsigned int closedown = 0;

	if( dualfifo_bytesAvailable( &conn->clientBuf, DUALFIFO_MODE_WRITE ) > PROXY_READ_SIZE )
	{
		bytesRead = read( clientSocket, readBuffer, PROXY_READ_SIZE );
	
		if( bytesRead > 0 )
		{
			dualfifo_write( &conn->clientBuf, readBuffer, bytesRead );
			
			// Queue proxy side write
			event_add( &conn->evProxyWrite, NULL );
		}
		else
		{
			// Write the connection off
			closedown = 1;
		}
	}

	if( closedown )
	{
		printf( "(%i) Remote peer closed\n", conn->id );
	
		event_del( &conn->evClientRead );
		
		close( conn->clientSocket );
		conn->clientSocket = -1;
				
		connection_teardown( conn );
		
		conn->state = CONN_STATE_CLOSE;
	}
	else
	{
		event_add( &conn->evClientRead, NULL );
	}
}

void connection_clientWrite( int clientSocket, short status, void * conn_v )
{
	struct conn_data * conn = (struct conn_data *) conn_v;
	char writeBuffer[ PROXY_WRITE_SIZE ];
	unsigned int bytesToWrite;
	size_t bytesWritten;

	if( conn->state == CONN_STATE_REMOTECONN )
	{
		conn->state = CONN_STATE_PROXY;	
	}
	
	bytesToWrite = dualfifo_peek( &conn->proxyBuf, DUALFIFO_MODE_READ, writeBuffer, PROXY_WRITE_SIZE );
	if( bytesToWrite > 0 )
	{	
		bytesWritten = send( clientSocket, writeBuffer, bytesToWrite, 0 );
		if( bytesWritten > 0 )
		{
			dualfifo_advance( &conn->proxyBuf, DUALFIFO_MODE_READ, bytesWritten );
		}
	}

	// Be more intelligent: only queue another write poll request if more data available
	if( dualfifo_bytesAvailable( &conn->proxyBuf, DUALFIFO_MODE_READ ) > 0 )
	{
		event_add( &conn->evClientWrite, NULL );
	}
}

char * connection_get_servName( FCGI_Header *request, unsigned int contentLength )
{
	unsigned char *ptr, *endPtr;
	char *name, *value;
	unsigned int nameLen, valueLen;
	char *serverName = NULL;
	char paramName[] = "SERVER_NAME"; // Hard-coded for now, will be user-set in the future
	
	if( request->type != FCGI_PARAMS )
	{
		return NULL;
	}
	
	ptr = (unsigned char* ) request + sizeof( FCGI_Header );
	endPtr = ptr + contentLength;
	while( ptr < endPtr )
	{	
		ptr = fcgi_getNextParam( ptr, &name, &nameLen, &value, &valueLen );
		
		if( nameLen == strlen( paramName ) && strncmp( name, paramName, nameLen ) == 0 )
		{
			serverName = strndup( value, valueLen );
			break;
		}
	}
	
	return serverName;
}

int connection_fcgiConnect( struct conn_data *conn )
{
	int clientSocket;
	int socketAddr_len;
	struct sockaddr_un socketAddr;
	
	clientSocket = socket( PF_UNIX, SOCK_STREAM, 0 );
	if( clientSocket < 0 )
	{
		printf( "Cannot create a UNIX socket to listen on!\n" );
		return -1;
	}
	
	conn->clientSocket = clientSocket;
	
	fcntl( clientSocket, F_SETFL, O_NONBLOCK ); 
	
	socketAddr.sun_family = AF_UNIX;
	strncpy( socketAddr.sun_path, conn->cgiHost, sizeof( socketAddr.sun_path ) - 1 );
	
	socketAddr_len = strlen( socketAddr.sun_path ) + 1 + sizeof( socketAddr.sun_family );
	
	if( connect( clientSocket, (struct sockaddr *) &socketAddr, socketAddr_len ) < 0 )
	{
		printf( "Cannot connect to socket %s \n", conn->cgiHost );
	}
	
	event_set( &conn->evClientRead,  clientSocket, EV_READ,  connection_clientRead , conn );
	event_set( &conn->evClientWrite, clientSocket, EV_WRITE, connection_clientWrite, conn );
	
	event_add( &conn->evClientRead,  NULL );
	event_add( &conn->evClientWrite, NULL );
	
	return 0;
}


	

	
